open Unix

open Common
open Io
open Ftp_session
open Ftp_core
open Ftp_file_info
open Error
open Glob

(******************************************************************************)

let is_opt arg =
  arg.[0]='-' && String.length arg > 1

let get_opt arg =
  assert (is_opt arg);
  String.sub arg 1 (String.length arg - 1)

let is_digit c =
  match c with
  | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' -> true
  | _ -> false

let parse_args args opts_without_arg opts_with_one_arg from =
  let argc = List.length args in
  let opts = ref [] in
  let remain_args = ref [] in
  let ignore_next_arg = ref false in
  let opt_compare = 
    fun opt -> fun candidate -> String.compare opt candidate = 0 
  in
  for i = from to (argc-1) do
    if not !ignore_next_arg then
      let arg = List.nth args i in
      if is_opt arg then
        (
         let opt = String.sub arg 1 (String.length arg -1) in
         if List.exists (opt_compare opt) opts_without_arg then
           opts := (opt, "") :: (!opts)
         else if List.exists (opt_compare opt) opts_with_one_arg then
           let next_arg =
             if i<(argc-1) then
               (
                ignore_next_arg := true;
                List.nth args (i+1)
               )
             else
               invalid_arg ("require another argument after -"^opt)
           in
           opts := (opt, next_arg) :: (!opts)
         else
           invalid_arg ("unrecognized option -"^opt)
        )
      else
        remain_args := arg :: (!remain_args)
  done;
  !opts, !remain_args

(******************************************************************************)

let expand_dir dir =
  assert (String.length dir > 0);
  if dir.[0]='~' then
    let home_dir = 
      try 
        Sys.getenv "HOME"
      with
      | Not_found ->
          (
           try 
             let uid = getuid () in
             let pwentry = getpwuid uid in
             pwentry.pw_dir
           with
           | _ -> "~"
          )
    in
    home_dir^"/"^(String.sub dir 1 (String.length dir - 1))
  else
    dir

(******************************************************************************)

let get_user_and_pass arg =
  let username = ref "" in
  let password = ref "" in
  let parsing_pass = ref false in
  let previous_escape = ref false in
  for i = 0 to (String.length arg)-1 do
    let c = arg.[i] in
    (
     match c with
     | '\\' ->
         if !previous_escape then
           (
            if !parsing_pass then
              password := !password ^ (String.make 1 c)
            else
              username := !username ^ (String.make 1 c);
            previous_escape := false
           )
         else
           previous_escape := true
     | ',' ->
         if !previous_escape then
           (
            previous_escape := false;
            if !parsing_pass then
              password := !password ^ (String.make 1 c)
            else
              username := !username ^ (String.make 1 c)
           )
         else
           parsing_pass := true
     | _ ->
         previous_escape := false;
         if !parsing_pass then
           password := !password ^ (String.make 1 c)
         else
           username := !username ^ (String.make 1 c)
    )
  done;
  !username, !password, !parsing_pass

let split_url url =
  if (String.compare (remove_heading_blank url) "")=0 then
    "", "", false
  else
    (
     let at_pos = ref (-1) in
     let loop_cond = ref true in
     let cur_pos = ref 0 in
     let previous_escape = ref false in
     let url_len = String.length url in
     while !loop_cond do
       let c = url.[!cur_pos] in
       (
        match c with
        | '\\' ->
            previous_escape := not !previous_escape
        | '@' ->
            at_pos := !cur_pos;
            loop_cond := !previous_escape;
            previous_escape := false;
        | _ ->
            previous_escape := false
       );
       incr cur_pos;
       loop_cond := !loop_cond && !cur_pos < url_len
     done;
     if !at_pos>0 then
       (String.sub url 0 !at_pos), 
       (String.sub url (!at_pos+1) (String.length url - !at_pos - 1)), 
       true
     else
       "", 
       (String.sub url (!at_pos+1) (String.length url - !at_pos -1)), 
       false
    )

let parse_url url =
  let auth, addr, found_auth = split_url url in
  let username = ref "" in
  let password = ref "" in
  let found_username = ref false in
  let found_password = ref false in
  let host = ref "" in
  let port = ref 0 in
  let found_port = ref false in
  if found_auth then
    (
     let previous_escape = ref false in
     for i = 0 to (String.length auth)-1 do
       let c = auth.[i] in
       match c with
       | '\\' ->
           if !previous_escape then
             (
              previous_escape := false;
              if !found_username then
                password := !password ^ (String.make 1 c)
              else
                username := !username ^ (String.make 1 c)
             )
           else
             previous_escape := true
       | ':' ->
           if !previous_escape then
             (
              previous_escape := false;
              if !found_username then
                password := !password ^ (String.make 1 c)
              else
                username := !username ^ (String.make 1 c)
             )
           else
             (
              found_username := true;
              found_password := true
             )
       | _ ->
           previous_escape := false;
           if !found_username then
             password := !password ^ (String.make 1 c)
           else
             username := !username ^ (String.make 1 c)       
     done;
     if (not !found_username) && (String.compare !username "")<>0 then
       found_username := true
    );
  let previous_escape = ref false in
  for i = 0 to (String.length addr)-1 do
    let c = addr.[i] in
    match c with
    | '\\' ->
        if !previous_escape then
          (
           previous_escape := false;
           if not !found_port then
             host := !host ^ (String.make 1 c)
           else
             invalid_arg addr
          )
        else
          previous_escape := true
    | ':' ->
        if !previous_escape then
          (
           previous_escape := false;
           if not !found_port then
             host := !host ^ (String.make 1 c)
           else
             invalid_arg addr
          )
        else
           found_port := true
    | _ ->
        previous_escape := false;
        if not !found_port then
          host := !host ^ (String.make 1 c)
        else
          (
           if is_digit c then
             port := !port * 10 + (int_of_string (String.make 1 c))
           else
             invalid_arg addr
          )
  done;
  if (String.compare (remove_heading_blank !host) "")=0 then
    invalid_arg "illegal address";
  !host, !port,       !username,       !password, 
         !found_port, !found_username, !found_password

let cmd_open args =
  assert ((String.compare (List.nth args 0) "open")=0);
  let hostname = ref "" in
  let port = ref 21 in
  let username = ref "anonymous" in
  let password = ref "mlftp@mail" in
  let need_username = ref true in
  let need_password = ref true in
  let need_port = ref true in
  let getpass () =
    print_normal_msg "Password: ";
    try
      password := get_pass ()
    with
    | Get_pass_failed ->
        print_error_msg "Get password failed.\n"
  in
  let opts, url = parse_args args [] ["u";"p"] 1 in
  let ignore_opt_u = ref false in
  let ignore_opt_p = ref false in
  for i = 0 to (List.length opts - 1) do
    let opt,arg = List.nth opts i in
    match opt.[0] with
    | 'u' ->
        if (not !ignore_opt_u)&&(!need_username) then
          (
           need_username := false;
           ignore_opt_u := true;
           let user, pass, found_pass = 
             get_user_and_pass arg
           in
           username := user;
           if found_pass then
             (
              password := pass;
              need_password := false
             )
           else
             (
              getpass ();
              need_password := false
             )
          )
    | 'p' ->
        if (not !ignore_opt_p)&&(!need_port) then
          (
           need_port := false;
           ignore_opt_p := true;
           port := (
             try
               int_of_string arg
             with
             | _ ->
                 invalid_arg "option -p requires a positive integral argument"
            );
           if !port<=0 then 
             invalid_arg "option -p requires a positive integral argument"
          )
    | _ -> (* this case should not happen *)
        invalid_arg ("unrecognized option -"^opt)
  done;
  let remain_argc = List.length url in
  if remain_argc=0 then
    invalid_arg "a url required"
  else if remain_argc>1 then
    invalid_arg "too many arguments"
  else
    (
     let arg = List.nth url 0 in
     let parse_host, parse_port, parse_user, parse_pass,
       found_port, found_user, found_pass =
       parse_url arg
     in
     hostname := parse_host;
     if (String.compare (remove_heading_blank !hostname) "")=0 then
       invalid_arg arg;
     if !need_port && found_port then
       port := parse_port;
     if !need_username && found_user then
       username := parse_user;
     if !need_password && found_pass then
       password := parse_pass;
     if !need_password && not found_pass then
       getpass ()
    );
  login !hostname !port !username !password ""  

(******************************************************************************)

let cmd_cd session args =
  assert ((String.compare (List.nth args 0) "cd")=0);
  let cc = session.cc in
  let argc = List.length args in
  let dir = ref "/" in
  let reply_code = 
    if argc>2 then
      invalid_arg "too many arguments"
    else if argc = 1 then
      (
       dir := "/";
       change_work_dir cc "/"
      )
    else
      (
       dir := List.nth args 1; 
       if !dir.[String.length !dir - 1]<>'/' then
         dir := !dir ^ "/";
       dir := get_absolute_path session.workdir !dir;
       change_work_dir cc !dir
      )
  in
  match int_of_string reply_code with
  | 250 ->
      session.workdir <- !dir;
      print_normal_msg ("Change to directory "^(!dir)^".\n")
  | 550 ->
      print_error_msg ("Directory "^(!dir)^" unavailable now.\n")
  | _ ->
      perror_for_reply_code (int_of_string reply_code)

(******************************************************************************)

let cmd_ls session args =
  assert ((String.compare (List.nth args 0) "ls")=0);
  let argc = List.length args in
  if argc>2 then
    invalid_arg "too many arguments"
  else
    let pathname =
      if argc=1 then
        "."
      else
        List.nth args 1
    in
    let reply_code =
      list_files session pathname
    in
    match int_of_string reply_code with
    | 226
    | 250 ->
        ()
    | 450 ->
        print_error_msg ("Directory or file "^pathname^" unavailable now.\n")
    | 502 ->
        print_error_msg ("LIST not implemented on remote.\n")
    | _ ->
        perror_for_reply_code (int_of_string reply_code)

let cmd_nlist session args =
  assert ((String.compare (List.nth args 0) "nlist")=0);
  let argc = List.length args in
  if argc>2 then
    invalid_arg "too many arguments"
  else
    let pathname =
      if argc=1 then
        "."
      else
        List.nth args 1
    in
    let reply_code =
      nlist_files session pathname
    in
    match int_of_string reply_code with
    | 226
    | 250 ->
        ()
    | 450 ->
        print_error_msg ("Directory or file "^pathname^" unavailable now.\n")
    | 502 ->
        print_error_msg ("NLST not implemented on remote.\n")
    | _ ->
        perror_for_reply_code (int_of_string reply_code)

(******************************************************************************)

let cmd_pwd session args =
  assert ((String.compare (List.nth args 0) "pwd")=0);
  let argc = List.length args in
  if argc>1 then
    invalid_arg "too many arguments"
  else
    print_normal_msg (session.workdir^"\n")

(******************************************************************************)

let cmd_mkdir cc args =
  assert ((String.compare (List.nth args 0) "mkdir")=0);
  let argc = List.length args in
  if argc=1 then
    invalid_arg "at least one pathname requried"
  else
    for i= 1 to List.length args do
      let pathname = List.nth args i in
      let reply_code = create_dir cc pathname in
      match int_of_string reply_code with
      | 257 ->
          print_normal_msg (pathname^" created successfully.\n")
      | 502 ->
          print_error_msg ("MKD not implemented on remote.\n")
      | 550 ->
          print_error_msg (pathname^" unavailable now.\n")
      | _ ->
          perror_for_reply_code (int_of_string reply_code)
    done

(******************************************************************************)

let cmd_quote cc args =
  assert ((String.compare (List.nth args 0) "quote")=0);
  let argc = List.length args in
  if argc=1 then
    invalid_arg "command required"
  else
    (
     let ftp_raw_cmd = ref (List.nth args 1) in
     for i = 2 to (List.length args)-1 do
       ftp_raw_cmd := !ftp_raw_cmd ^ " " ^ (List.nth args i)
     done;
     print_normal_msg (quote cc !ftp_raw_cmd)
    )

(******************************************************************************)

let cmd_rm session args =
  assert ((String.compare (List.nth args 0) "rm")=0);
  let cc = session.cc in
  let argc = List.length args in
  if argc=1 then
    invalid_arg "at least one directory or file name required"
  else
    let rec remove_r pathname =
      (* try to delete pathname as a regular file or a symlink *)
      let reply_code = int_of_string (remove_file cc pathname) in
      match reply_code with
      | 250 ->
          ()
      | 450
      | 550 -> (* remote file is neither a regular file nor a symlink *)
          (* try to remove it as a directory *)
          let reply_code, file_list = get_file_list session pathname in
          if reply_code.[0]='2' then (* delete files in directory pathname *)
            let reply_code = change_work_dir cc pathname in
            if reply_code.[0]='2' then
              (
               for i = 0 to (List.length file_list - 1) do
                 let subpathname = List.nth file_list i in
                 if (not (String.compare subpathname "." = 0)) && 
                   (not (String.compare subpathname ".." = 0)) then
                   remove_r subpathname
               done;
               ignore (change_work_dir cc "..")
              )
            else
              print_error_msg ("Cannot change workdir to directory "^pathname^".\n")
          else
            print_error_msg ("Cannot get the file list of directory "^pathname^". Try to remove it directly.\n");
          let reply_code = int_of_string (remove_dir cc pathname) in
          (
           match reply_code with
           | 250 ->
               ()
           | 450
           | 550 ->
               print_error_msg ("Directory "^pathname^" unavailable now or not empty.\n")
           | 502 ->
               print_error_msg ("RMD not implementedd on remote.\n")
           | _ ->
               perror_for_reply_code reply_code
          )
      | _ ->
          perror_for_reply_code reply_code       
    in
    let opts, files = parse_args args ["r"] [] 1 in
    let r_flag = (List.length opts > 0) in
    let files_number = List.length files in
    if files_number=0 then
      invalid_arg "at least one directory or file name required"
    else
      for i = 0 to (files_number-1) do
        let pathname = List.nth files i in
        if not r_flag then
          let reply_code = int_of_string (remove_file cc pathname) in
          match reply_code with
          | 250 ->
              ()
          | 450
          | 550 ->
              print_error_msg ("File "^pathname^" is a directory.\n")
          | 502 ->
              print_error_msg ("DELE not implemented on remote.\n")
          | _ ->
              perror_for_reply_code reply_code
        else
          remove_r pathname
      done

(******************************************************************************)

let cmd_site cc args =
  assert ((String.compare (List.nth args 0) "site")=0);
  let argc = List.length args in
  if argc=1 then
    invalid_arg "more arguments required"
  else
    let args_str = ref "" in
    for i = 1 to (argc-1) do
      args_str := !args_str ^ (List.nth args i)
    done;
    print_normal_msg (site cc !args_str)

(******************************************************************************)

let cmd_user session args =
  assert ((String.compare (List.nth args 0) "user")=0);
  let argc = List.length args in
  if argc=1 then
    invalid_arg "username required"
  else if argc>2 then
    invalid_arg "too many arguments"
  else
    let username = List.nth args 1 in
    let password = 
      print_normal_msg "Password: ";
      get_pass ()
    in
    let cc = session.cc in
    let reply_code = 
      int_of_string (get_reply_code (feed_command cc (USER username)))
    in
    let user_chg_ok acct =
      let authinfo = session.auth in
      authinfo.username <- username;
      authinfo.password <- password;
      authinfo.acctinfo <- acct
    in
    let user_chg_fail () =
      print_error_msg "User changed failed. User unchanged.\n"
    in
    let handle_acct () =
      print_normal_msg "Acct: ";
      let acct = get_pass () in
      let reply_code =
        get_reply_code (feed_command cc (ACCT acct))
      in
      (
       match reply_code.[0] with
       | '2' ->
           user_chg_ok acct
       | _ ->
           user_chg_fail ()
      )
    in
    match reply_code with
    | 331 ->
        let reply_code = 
          int_of_string (get_reply_code (feed_command cc (PASS password)))
        in
        (
         match reply_code with
         | 230 ->
             user_chg_ok ""
         | 332 ->
             handle_acct ()
         | _ ->
             user_chg_fail ()
        )
    | 332 ->
        handle_acct ()
    | 230 ->
        user_chg_ok ""
    | _ ->
        user_chg_fail ()

(******************************************************************************)

let cmd_get session args =
  assert ((String.compare (List.nth args 0) "get")=0);
  let argc = List.length args in
  if argc=1 then
    invalid_arg "file name required"
  else
    let opts, file = parse_args args ["c"] ["o"] 1 in
    let c_flag = ref false in
    let o_flag = ref false in
    let o_file = ref "" in
    for i = 0 to (List.length opts - 1) do
      let opt,opt_arg = List.nth opts i in
      match opt.[0] with
      | 'c' -> 
          if not !c_flag then
            c_flag := true
      | 'o' ->
          if not !o_flag then
            (
             o_flag := true;
             o_file := expand_dir opt_arg;
             if !o_file.[String.length !o_file - 1]='/' && 
                not (Sys.file_exists !o_file) then
               invalid_arg ("directory "^(!o_file)^" not exists")
            )
      | _ -> (* this case should not happen *)
          invalid_arg ("unrecognized option -"^opt)
    done;
    let file_number = List.length file in
    if file_number=0 then
      invalid_arg "file name required"
    else if file_number>1 then
      invalid_arg "too many arguments"
    else
      let reply_code = int_of_string
        (retrieve_file session (List.nth file 0) !o_file false !c_flag)
      in
      match reply_code with
      | 226
      | 250 ->
          print_normal_msg "Transfer complete.\n"
      | _ ->
          perror_for_reply_code reply_code

let cmd_mget session args =
  assert ((String.compare (List.nth args 0) "mget")=0);
  let argc = List.length args in
  if argc=1 then
    invalid_arg "file name required"
  else
    let opts, files = parse_args args ["c"] [] 1 in
    let c_flag = ref false in
    for i = 0 to (List.length opts - 1) do
      let opt,_ = List.nth opts i in
      match opt.[0] with
      | 'c' ->
          if not !c_flag then
            c_flag := true
      | _ -> (* this case should not happen *)
          invalid_arg ("unrecognized option -"^opt)
    done;
    let file_number = List.length files in
    if file_number=0 then
      invalid_arg "file name required"
    else 
      for i = 0 to (file_number - 1) do
        let filename = List.nth files i in
        let glob_files = rmglob session filename in
        for j = 0 to (List.length glob_files - 1) do
          let file_to_retrieved = List.nth glob_files j in
          let reply_code = int_of_string
              (retrieve_file session file_to_retrieved "" false !c_flag)
          in
          match reply_code with
          | 226
          | 250 ->
              print_normal_msg ("Transfer compelete:"^file_to_retrieved^"\n")
          | _ ->
              perror_for_reply_code reply_code
        done
      done

(******************************************************************************)

let cmd_put session args =
  assert ((String.compare (List.nth args 0) "put")=0);
  let argc = List.length args in
  if argc=0 then
    invalid_arg "file name required"
  else
    let opts,file = parse_args args ["c"] ["o"] 1 in
    let c_flag = ref false in
    let o_flag = ref false in
    let o_file = ref "" in
    for i = 0 to (List.length opts - 1) do
      let opt,opt_arg = List.nth opts i in
      match opt.[0] with
      | 'c' ->
          if not !c_flag then
            c_flag := true
      | 'o' ->
          if not !o_flag then
            (
             o_flag := true;
             o_file := opt_arg
            )
      | _ -> (* this case should not happen *)
          invalid_arg ("unrecognized option -"^opt)
    done;
    let file_number = List.length file in
    if file_number=0 then
      invalid_arg "file name required"
    else if file_number>1 then
      invalid_arg "too many arguments"
    else
      let local_file = expand_dir (List.nth file 0) in
      if not (Sys.file_exists local_file) then
        print_error_msg ("File "^local_file^" not exists.\n")
      else
        let reply_code = int_of_string
            (store_file session local_file !o_file false !c_flag)
        in
        match reply_code with
        | 226
        | 250 ->
            print_normal_msg "Transfer complete.\n"
        | _ ->
            perror_for_reply_code reply_code

let cmd_mput session args =
  assert ((String.compare (List.nth args 0) "mput")=0);
  let argc = List.length args in
  if argc=0 then
    invalid_arg "file name required"
  else
    let opts,files = parse_args args ["c"] [] 1 in
    let c_flag = ref false in
    for i = 0 to (List.length opts - 1) do
      let opt,_ = List.nth opts i in
      match opt.[0] with
      | 'c' ->
          if not !c_flag then
            c_flag := true
      | _ -> (* this case should not happen *)
          invalid_arg ("unrecognized option -"^opt)
    done;
    let file_number = List.length files in
    if file_number=0 then
      invalid_arg "file name required"
    else
      for i = 0 to (file_number - 1) do
        let filename = List.nth files i in
        let glob_files = glob filename in
        for j = 0 to (Array.length glob_files - 1) do
          let file_to_stored = glob_files.(j) in
          let reply_code = int_of_string
              (store_file session file_to_stored "" false !c_flag)
          in
          match reply_code with
          | 226
          | 250 ->
              print_normal_msg ("Transfer complete: "^file_to_stored^"\n")
          | _ ->
              perror_for_reply_code reply_code
        done
      done
      
(******************************************************************************)

let cmd_lcd args =
  assert ((String.compare (List.nth args 0) "lcd")=0);
  let argc = List.length args in
  if argc=1 then
    invalid_arg "pathname required"
  else if argc>2 then
    invalid_arg "too many arguments"
  else
    let dir = expand_dir (List.nth args 1) in
    try
      Sys.chdir dir
    with
    | Sys_error msg ->
        print_error_msg (msg^"\n")

(******************************************************************************)

let cmd_shell args =
  assert ((List.nth args 0).[0]='!');
  let arg0 = List.nth args 0 in
  let shell_cmd = ref (String.sub arg0 1 (String.length arg0 - 1)) in
  let argc = List.length args in
  for i = 1 to (argc-1) do
    shell_cmd := !shell_cmd ^ " " ^ (List.nth args i)
  done;
  ignore (Sys.command !shell_cmd)

(******************************************************************************)

let cmd_help args =
  assert ((String.compare (List.nth args 0) "help")=0);
  print_normal_msg "TODO\n"

(******************************************************************************)

let is_on_off arg = 
  (String.compare arg "on" = 0) || (String.compare arg "off" = 0)

let get_bool_val arg =
  if arg.[1]='n' then
    true
  else
    false

let cmd_set session args =
  assert ((String.compare (List.nth args 0) "set")=0);
  let argc = List.length args in
  if argc=1 then
    (
     print_normal_msg ("debug: "^(string_of_bool !debug)^"\n");
     print_normal_msg ("quiet: "^(string_of_bool !quiet)^"\n");
     print_normal_msg ("passive: "^(string_of_bool session.passive)^"\n");
     print_normal_msg ("server charset: "^(session.charset)^"\n");
     print_normal_msg ("console charset: "^(!console_charset)^"\n");
     print_normal_msg ("retransfer timer: "^(string_of_float !retransfer_timer)^"s\n")
    )
  else if argc=3 then
    let item = List.nth args 1 in
    let value = List.nth args 2 in
    match item with 
    | "server_charset" ->
        ignore (session.charset <- value);
        server_charset := value
    | "console_charset" ->
        console_charset := value
    | "debug" ->
        if is_on_off value then
          debug := get_bool_val value
        else
          invalid_arg "invalid setting"
    | "quiet" ->
        if is_on_off value then
          quiet := get_bool_val value
        else
          invalid_arg "invalid setting"
    | "passive" ->
        if is_on_off value then
          ignore (session.passive <- get_bool_val value)
        else
          invalid_arg "invalid setting"
    | "retransfer_timer" ->
        (
         try
           let timer_out = float_of_string value in
           retransfer_timer := timer_out
         with
         | _ -> 
             invalid_arg "invalid setting"
        )
    | _ ->
        invalid_arg "invalid setting"
  else
    invalid_arg "invalid setting"
    
